22.11.1 HandlerExceptionResolver

Spring HandlerExceptionResolver的实现用来处理在控制器执行时发生的意外的异常。HandlerExceptionResolver有些类似于你在web程序的web.xml中定义了异常的映射。但是,它提供了一种更灵活的做法。比如他们提供了哪个处理器在执行时抛出异常的信息。此外,编码的方式去处理异常给了你在请求转发到另一个URL前更多机会选择合适的响应(和使用Servlet特定的异常映射有相同的结果)。
除了实现HandlerExceptionResolver接口(关键是实现resolveException(Exception, Handler)方法和返回一个ModelAndView),你还可以使用Spring提供的SimpleMappingExceptionResolver或是创造一个带@ExceptionHandler注解的方法。SimpleMappingExceptionResolver使你可以将任何可能抛出的异常类名映射到一个视图。这和Servlet API的异常映射是一样的,但你也可以实现对来自不同处理器的异常更细粒度的映射。另一方面,@ExceptionHandler注释可以用于处理异常的方法上。这样的方法可以被定义在@Controller内本地使用或是定义在@ControllerAdvice内应用到多个@Controller上。下面一节会解释更多的细节。

22.11.2 @ExceptionHandler

HanlderExceptionResolver接口和SimpleMappingExceptionResolver实现允许你在映射异常到特定的视图时,可以在映射到视图前选择声明一些Java逻辑。然而,某些情况下,特别是依赖@ResponseBody方法而不是视图解析时,直接设置响应的状态并选择性的写一些错误到响应体中或更便捷。
你可以通过@ExceptionHandler方法实现这点。当控制器内声明这样的方法,同个控制器(或是它的子类)中的@RequestMapping方法引发了异常时,该方法会被应用。你也可以将@ExceptionHandler方法声明在@ControllerAdvice类内部,这样它会处理多个控制器@RequestMapping方法的异常,下面的例子是一个控制器内的@ExceptionHandler方法:

@Controller
public class SimpleController {

    // @RequestMapping methods omitted ...

    @ExceptionHandler(IOException.class)
    public ResponseEntity<String> handleIOException(IOException ex) {
        // prepare responseEntity
        return responseEntity;
    }

}

@ExceptionHandler的值可以设置为异常类型的数组。如果抛出的异常符合这个列表中的异常类型,那么被注释@ExceptionHandler的方法会被调用。如果没有设置异常类型的列表,那么会使用方法参数的异常类型。

对于@ExceptionHandler方法,无论该方法是咋特定的控制器还是advice bean中,根异常匹配都将优先于当前异常原因的匹配。但是对于一个更高优先级的@ControllerAdvice仍然会优先于其他低优先级的advice bean中的匹配(无论是根还是原因的等级)。因此,当使用多个advice时,请以异常相对应的顺序声明映射的advice bean顺序!

与带@RequestMapping注解的标准的控制器方法相似,@ExceptionHandler方法的参数和返回值也可以很灵活。比如,在Servlet环境中可以访问HttpServletReqeust,而在Portlet环境中可以访问PortletRequest。返回类型可以是一个String,被用来检测视图名;可以是一个ModelAndView对象;一个ResponseEntity;甚至你可以添加@ResponseBody让返回值通过消息转换器转换并写入响应流。

22.11.3 Handling Standard Spring MVC Exceptions

Spring MVC可能会在处理请求的时候引发一系列的异常。SimpleMappingExceptionResolver可以轻易的将任意异常映射到默认错误视图。然而,当使用自动解析响应的客户端时,你会希望直接设置特定的响应状态码。状态码会根据抛出的异常被设置成客户端错误(4XX)或是服务器错误(5XX)。
DefaultHandlerExceptionResolver会将Spring MVC的异常转换成特定的错误状态码。它默认注册到被MVC命名空间,MVC Java配置和DispatcherServlet(即无论是使用MVC命名空间还是Java配置)。下面列出了这个解析器处理的异常及对应的状态码:

Exception HTTP Status Code
BindException 400(Bad Request)
ConversionNotSupportedException 500(Internal Server Error)
HttpMediaTypeNotAcceptableException 406(Not Acceptable)
HttpMediaTypeNotSupportedException 415(Unsupported Media Type)
HttpMessageNotReadableException 400(Bad Request)
HttpMessageNotWritableException 500(Internal Server Error)
HttpRequestMethodNotSupportedException 405(Method Not Allowed)
MethodArgumentNotValidException 400(Bad Request)
MissingPathVariableException 500(Internal Server Error)
MissingServletRequestParameterException 400(Bad Request)
MissingServletRequestPartException 400(Bad Request)
NoHandlerFoundException 404(Not Found)
NoSuchRequestHandlingMethodException 404(Not Found)
TypeMismatchException 400(Bad Request)

DefaultHandlerExceptionResolver透明的设置响应状态。但是,当你的应用程序需要添加对开发者友好的内容到响应中去时(比如提供REST API时),它不会将任何的错误内容写入到响应体。你可以准备一个ModelAndView并通过视图解析渲染错误内容——即,通过配置ContentNegotiatingViewResolverMappingJackson2JsonView等等。然而,你可能还是更喜欢使用@ExceptionHandler方法代替。
如果你更喜欢通过@ExceptionHandler写入错误内容,你可以继承ResponseEntityExceptionHandler。这是个继承自带@ControllerAdvice的类并提供了@ExceptionHandler方法来处理异常并返回ResponseEntity。它使你可以自定义响应并通过消息转换器写入错误内容。更多信息见ResponseEntityExceptionHandler

22.11.4 Annotating Business Exceptions With @ResponseStatus

业务逻辑的异常可以带@ResponseStatus。当这个产生异常时,ResponseStatusExceptionResolver会将响应设为响应的状态值。DispatcherServler默认会注册ResponseStatusExceptionResolver,因此它是可用的。

22.11.5 Customizing the Default Servlet Container Error Page

当响应的状态被设置成错误状态码且响应体为空时,Servlet容器通常会渲染格式化的HTML错误页。为了自定义容器的默认错误页,你可以在web.xml中声明<error-page>属性。在Servlet 3之前,这个元素不得不映射特定的状态或是异常。从Servelt 3开始,错误页面不再需要被映射,这实际上意味着指定的位置会自定义默认的Servlet容器错误页面。

<error-page>
    <location>/error</location>
</error-page

注意,错误页面的实际位置可以使一个JSP或是容器内通过@Controller方法映射的URL:当写错误信息是,HttpServletResponse中设置的错误状态码和错误消息可以在控制器中通过请求的属性获得:

@Controller
public class ErrorController {

    @RequestMapping(path = "/error", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    @ResponseBody
    public Map<String, Object> handle(HttpServletRequest request) {

        Map<String, Object> map = new HashMap<String, Object>();
        map.put("status", request.getAttribute("javax.servlet.error.status_code"));
        map.put("reason", request.getAttribute("javax.servlet.error.message"));

        return map;
    }

}

或是使用JSP:

<%@ page contentType="application/json" pageEncoding="UTF-8"%>
{
    status:<%=request.getAttribute("javax.servlet.error.status_code") %>,
    reason:<%=request.getAttribute("javax.servlet.error.message") %>
}